level 6 - 10
Uploading remaining 5 crackmes under 1
This writeup covers the remaining levels from 6 to 10. Each level continues to introduce new checks: out-of-order byte checks, a 3-branch verification loop, multi-byte algebraic constraints, and multithreading with a rolling string hash. Across all the levels, flag gen comes first, then length check and finally branching to their own individual checks.
Level 6: Out-of-order character verification
Checks a 5-letter key, but instead of checking the string sequentially, it verifies the characters out of order across the input buffer.

Looking at the disassembly, the verification steps are:
- Index 0 (byte ptr [rax]): checked against 0x63 ('c')
- Index 4 (byte ptr [rax+4]): checked against 0x6B ('k')
- Index 1 (byte ptr [rax+1]): checked against 0x72 ('r')
- Index 3 (byte ptr [rax+3]): checked against 0x63 ('c')
- Index 2 (byte ptr [rax+2]): checked against 0x61 ('a')
Reconstructing the indices in order (0, 1, 2, 3, 4) gives the password: crack.

Level 7: XOR encrypted password check
Involves a check against an XOR encrypted password.



If length check matches, it runs an XOR verification loop:
- Loads our input character: movzx eax, byte ptr [rax+rcx]
- Encrypts it: xor al, 5Ah
- Compares the encrypted byte against the stored reference byte: cmp al, [rcx+rdx]
- Loops until all characters are checked
XORing the stored reference bytes with 0x5A gives the password: license.

The menu is just used as a manual push button to exit the program.
Level 8: 3 intervals, 3 branches (switch case)
Involves a looped verification check that branches based on a 54-byte array.
Part 2: Password Verification Loop
After flag generation, it moves on to verify our password against a 54-byte constant array loaded into R10.
This 54-byte array is structured into 18 intervals of 3 bytes each (18 * 3 = 54 bytes). The loop steps through this array one 3-byte interval at a time (add rdx, 3).

In every 3-byte interval, the first byte acts as an interval selector (1, 2, or 3) that dictates which branch the program takes:
- Interval 1 (fetch_password_char_by_char): When the interval selector is 1, it takes the index byte and fetches the corresponding character from our input password one by one (movzx r9d, byte ptr [rax+r8]).
- Interval 2 (pass_encryption): When the interval selector is 2, it takes the input password char and XORs it with a key byte from the interval (xor r9b, r8b).
- Interval 3 (verify_pass_encryption_char_by_char): When the interval selector is 3, it compares the XOR result against the expected byte (cmp r9b, r8b). If they match, cmovz ecx, eax preserves 1 in r11b.
At the end of the loop, test r11b, r11b checks r11. If any character check failed, r11 is 0 and branches to sad_path. If all passed, r11 is 1 and it takes the happy path.
Password Recovery
To find each expected character, we reverse the XOR by taking the key byte and the expected result byte from the array. At runtime, the binary takes our input character, XORs it with the key byte, and compares the result to the expected byte:
| Char Index | Key Byte | Expected Byte | Reverse Math (Key ^ Expected) | Plaintext Char | Runtime Check (Char ^ Key == Expected) |
|---|---|---|---|---|---|
| 0 | 0x12 | 0x7F | 0x12 ^ 0x7F = 0x6D | 'm' | 'm' (0x6D) ^ 0x12 == 0x7F |
| 1 | 0x34 | 0x55 | 0x34 ^ 0x55 = 0x61 | 'a' | 'a' (0x61) ^ 0x34 == 0x55 |
| 2 | 0x56 | 0x22 | 0x56 ^ 0x22 = 0x74 | 't' | 't' (0x74) ^ 0x56 == 0x22 |
| 3 | 0x78 | 0x0A | 0x78 ^ 0x0A = 0x72 | 'r' | 'r' (0x72) ^ 0x78 == 0x0A |
| 4 | 0x90 | 0xF9 | 0x90 ^ 0xF9 = 0x69 | 'i' | 'i' (0x69) ^ 0x90 == 0xF9 |
| 5 | 0xBC | 0xC4 | 0xBC ^ 0xC4 = 0x78 | 'x' | 'x' (0x78) ^ 0xBC == 0xC4 |
This gives the full 6-letter password: matrix.

Level 9: 32-bit algebra gymnastics
Takes an 8-letter password and divides it into two 32-bit integers, subjecting them to arithmetic and bitwise constraints.

The program splits the 8-byte input into two 4-byte halves:
- mov ecx, [rax]: First 4 bytes (X)
- mov edx, [rax+4]: Second 4 bytes (Y)
The verification sequence is:
lea eax, [rdx+rcx] ; eax = Y + X
xor edx, eax ; edx = Y ^ (Y + X)
lea ecx, [rdx+1337h] ; ecx = (Y ^ (Y + X)) + 0x1337
xor ecx, eax ; ecx = ((Y ^ (Y + X)) + 0x1337) ^ (Y + X)
cmp ecx, 656D4278h ; must equal 0x656D4278
jnz sad_path
lea eax, [rdx+rcx] ; eax = edx + ecx
cmp eax, 22F8D52Ch ; must equal 0x22F8D52C
jnz sad_path
let us work from known to unknown, all we have are two constants -> 0x656D4278 and 0x22F8D52C. We know the final correct value inside eax should be 0x22F8D52C. To form eax we add edx and ecx. We know ecx must be 0x656D4278, so:
- edx = 0x22F8D52C - 0x656D4278 = 0xBD8B92B4 (result of the first XOR op)
- (edx + 0x1337) ^ eax = 0x656D4278, which gives eax = 0xD8E6E793 (sum of the two halves)
- EDX = 0xBD8B92B4 ^ 0xD8E6E793 = 0x656D7527 (the last 4 bytes at rax+4)
- ECX = 0xD8E6E793 - 0x656D7527 = 0x7379726C (the first 4 bytes at rax)
Converting both values to little-endian byte order:
- First 4 bytes (ECX = 0x7379726C): in little-endian bytes 6C 72 79 73, which is lrys
- Last 4 bytes (EDX = 0x656D7527): in little-endian bytes 27 75 6D 65, which is 'ume
Placing the first 4 bytes followed by the last 4 bytes gives the full 8-letter password: lrys'ume.

Level 10: Multithreading and rolling string hash
In the main function, the check itself is clear: it compares a value in eax against 0x3D17141A. But where did the eax value come from? There was no calculation loop in the main function itself, yet the value being compared kept changing based on our password input.
Tracing back through main revealed a call to _Thrd_join - the main program was waiting for a background worker thread:

To find where the hash was being written, I checked cross-references (xrefs) on the global variable dword_7FF6C38E6278 and looked for the instruction with type w (Write): to see what instruction is writing to it.

The w xref pointed directly to sub_7FF6C38E1300+86, which was moving a value from ebx to the variable and i eventually tracked what was setting ebx and landed on the worker thread function containing the calculation loop:

Inside the worker function, the loop (hash_gen:) computes a rolling hash across each character:
movsx eax, byte ptr [rdx] ; Load current character
inc rdx
imul ebx, 1Fh ; multiply by 31 (0x1F)
add ebx, eax ; add next character
cmp rdx, rcx
jnz short hash_gen
In the forward direction, each cycle is just:
- a * 0x1F = c (where a is the running hash, and 0x1F is 31 in decimal)
- c + d = e (where d is the ASCII value of the next character, and e is the new hash)

Once computed, the worker thread writes the final hash into dword_7FF6A9536278 and unlocks its mutex (_Mtx_unlock).

Back in the main thread, once _Thrd_join finishes, it reads that global variable and checks if it equals 0x3D17141A.
Once again working from known to unknown. Our final target hash is 0x3D17141A, which is 1,024,922,650 in decimal.
Reversing the cycle equation: 1,024,922,650 - d = 31 * a
Dividing 1,024,922,650 by 31: 1,024,922,650 / 31 = 33,062,020 with a remainder of 30.
For (1,024,922,650 - d) to be cleanly divisible by 31, our character d must be 30 plus multiples of 31:
- 30 + 31 = 61 (which is '=', hex 0x3D)
- Then a = (1,024,922,650 - 61) / 31 = 33,062,019 (0x01F884E3)
Now 33,062,019 becomes our new target hash, and we repeat this exact same step backwards until the hash reaches 0.

Wrote a script that does the math for me, and because of the multiples of 31 at each step, the reverse script produces 243 valid strings.
